use std::{
    sync::LazyLock,
};
use bittle::{Bits, BitsMut};
use rand;
use crate::{
    defines::*,
};

include!("../table.rs");

const ZOBRIST_LEN:usize = BOARD_LEN * 3;
static ZOBRIST:LazyLock<[u64; ZOBRIST_LEN]> = LazyLock::new(||{
    let mut a = [0u64; ZOBRIST_LEN];
    for i in 0..ZOBRIST_LEN {
        a[i] = rand::random_range(0..=u64::MAX)
    }
    a
});

static INIT_BOARD:LazyLock<Board> = LazyLock::new(||{
    let mut board = Board::default();
    board.set(44, Piece::Black);
    board.set(55, Piece::Black);
    board.set(45, Piece::White);
    board.set(54, Piece::White);
    board
});

pub fn loc_bits_to_vec(bits:u128) -> Vec<Loc> {
    let mut locs = Vec::with_capacity(MOVES_FIXED_SIZE);
    for i in 0..BOARD_LEN as u32 {
        if bits.test_bit_le(i) {
            locs.push(i as Loc);
        }
    }
    locs
}

#[derive(Debug,Clone,Copy,PartialEq,Eq)]
pub struct Board {
    pub(crate) bits_piece: u128,
    pub(crate) bits_color: u128,
    hash: u64,
}
impl Default for Board {
    fn default() -> Self {
        Self {
            bits_piece: 0,
            bits_color: 0,
            hash: 0,
        }
    }
}
impl Board {
    #[inline] pub fn load(&mut self, from:&Board) -> &mut Self {
        self.bits_piece = from.bits_piece;
        self.bits_color = from.bits_color;
        self.hash = from.hash;
        self
    }

    #[inline] pub fn hash(&self) -> u64 {
        self.hash
    }

    #[inline] pub fn get(&self, loc:Loc) -> Piece {
        let bit_piece = self.bits_piece.test_bit_le(loc) as u32;
        let bit_color = self.bits_color.test_bit_le(loc) as u32;
        unsafe { std::mem::transmute(bit_piece + (bit_piece & bit_color)) }
    }

    #[inline] pub fn set_by_old(&mut self, loc:Loc, p:Piece, old:Piece) -> &mut Self {
        self.hash ^= ZOBRIST[old as usize * BOARD_LEN];
        match p {
            Piece::Empty => {
                self.bits_piece.clear_bit_le(loc);
            }
            Piece::Black => {
                self.bits_piece.set_bit_le(loc);
                self.bits_color.clear_bit_le(loc);
            }
            Piece::White => {
                self.bits_piece.set_bit_le(loc);
                self.bits_color.set_bit_le(loc);
            }
        }
        self.hash ^= ZOBRIST[p as usize * BOARD_LEN];
        self
    }
    #[inline] pub fn set(&mut self, loc:Loc, p:Piece) -> &mut Self {
        self.set_by_old(loc, p, self.get(loc))
    }

    #[inline] pub fn n_black(&self) -> usize {
        let mask = (self.bits_piece ^ self.bits_color) & self.bits_piece;
        mask.count_ones() as usize
    }

    #[inline] pub fn n_white(&self) -> usize {
        let mask = self.bits_piece & self.bits_color;
        mask.count_ones() as usize
    }

    #[inline] pub fn n_piece(&self) -> usize {
        self.n_black() + self.n_white()
    }

    #[inline] fn can_move_way(&self, lt:&[u8;8], i_lt:usize, side:u32, n:usize) -> usize {
        let loc = lt[i_lt] as Loc;
        let p = self.get(loc) as u32;
        if ((LOC_WAY_MASK[loc as usize][i_lt] as u32 * side) & (p & side)) == 0 {
            return (p & (3-side)) as usize * n
        }
        return self.can_move_way(&LOC_WAY_TABLE[loc as usize], i_lt, side, n+1)
    }
    #[inline] fn _can_move(&self, loc:Loc, pcur:Piece) -> bool {
        let side = pcur.side() as u32;
        let center = &LOC_WAY_TABLE[loc as usize];
        let mut n_all = 0;
        for i_lt in 0..center.len() {
            n_all += self.can_move_way(center, i_lt, side, 0);
        }
        return n_all > 0
    }
    #[inline] pub fn can_move(&self, loc:Loc, pcur:Piece) -> bool {
        if self.get(loc) != Piece::Empty {return false}
        return self._can_move(loc, pcur)
    }

    fn moves_line(&self, line_locs:&[u8;BOARD_SIZE], pcur:Piece, bits:&mut u128) {
        let mut ps_n = BOARD_SIZE;
        let ps = {
            let mut ps:[u8;BOARD_SIZE] = [0u8;BOARD_SIZE];
            ps[0] = self.get(line_locs[0] as u32) as u8;
            for i in 1..BOARD_SIZE {
                let p = self.get(line_locs[i] as u32) as u32;
                if line_locs[i] == line_locs[0] {
                    ps_n = i; break;
                }
                let is_end = (line_locs[i] == line_locs[0]) as i32;
                ps[i] = (p & ((is_end - 1) as u32)) as u8;
            }
            ps
        };
        let state_i = {
            let mut state_i = 0;
            let mut base = 1;
            for i in 0..BOARD_SIZE {
                state_i += (ps[i] as u32) * base;
                base *= 3;
            }
            state_i as usize
        };
        let move_bits = LINE_MOVE_BITS[state_i];
        let n_shift = (pcur as usize - 1) * 10;
        for i in 0..ps_n {
            let loc = line_locs[i];
            let mask = 1 << (i + n_shift);
            let mask_hit = ((move_bits & mask) == mask) as u32;
            let join = mask_hit;
            *bits |= (BOARD_HEAD_BIT >> (loc as usize)) * join as u128;
        }
    }
    #[inline] pub fn moves_bits(&self, pcur:Piece) -> u128 {
        let mut bits:u128 = 0;
        for i in 0..LINE_LOCS.len() {
            self.moves_line(&LINE_LOCS[i], pcur, &mut bits);
        }
        bits
    }
    #[inline] pub fn moves(&self, pcur:Piece) -> Vec<Loc> {
        loc_bits_to_vec(self.moves_bits(pcur))
    }

    #[inline] fn swap_way(&mut self, lt:&[u8;8], i_lt:usize, side:u32) -> bool {
        let loc = lt[i_lt] as u32;
        let p = self.get(loc) as u32;
        if ((LOC_WAY_MASK[loc as usize][i_lt] as u32 * side) & (p & side)) == 0 {
            let ptb = p & PIECE_SIDE_MAP[side as usize] as u32;
            PIECE_TO_BOOL[ptb as usize]
        } else if self.swap_way(&LOC_WAY_TABLE[loc as usize], i_lt, side) {
            self.set_by_old(loc,  PIECE_SIDE_MAP[side as usize], PIECE_ORDER_MAP[p as usize]);
            true
        } else {
            false
        }
    }

    #[inline] fn swap(&mut self, loc:Loc, pcur:Piece) {
        let side = pcur.side() as u32;
        let center = LOC_WAY_TABLE[loc as usize];
        self.swap_way(&center, 0, side);
        self.swap_way(&center, 1, side);
        self.swap_way(&center, 2, side);
        self.swap_way(&center, 3, side);
        self.swap_way(&center, 4, side);
        self.swap_way(&center, 5, side);
        self.swap_way(&center, 6, side);
        self.swap_way(&center, 7, side);
    }

    #[inline] pub fn down(&mut self, loc:Loc, pcur:Piece) {
        debug_assert!(loc_in_board(loc) && (self.get(loc) == Piece::Empty));
        self.set_by_old(loc, pcur, Piece::Empty);
        self.swap(loc, pcur);
    }
}
impl Board {
    pub fn make_init_board() -> Board {
        INIT_BOARD.clone()
    }
    pub fn make_random_board(init_board:Board, n_piece_min:usize, n_piece_max:usize) -> Board {
        let mut board = init_board;
        let board_n_piece = board.n_piece();
        let n_piece = rand::random_range(n_piece_min..=n_piece_max);
        let mut n_piece =  n_piece.max(board_n_piece) - board_n_piece;
        let mut n_pass = 0;
        let mut side = Piece::Black;
        while (n_piece > 0) && (n_pass < 1) {
            let moves = board.moves(side);
            if moves.is_empty() {
                n_pass += 1
            } else {
                let loc = moves[rand::random_range(0..moves.len())];
                board.down(loc, side);
                n_piece -= 1;
                n_pass = 0;
            }
            side = side.side();
        }
        board
    }
    pub fn make_random_board_by_init(n_piece_min:usize, n_piece_max:usize) -> Board {
        Self::make_random_board(Self::make_init_board(), n_piece_min, n_piece_max)
    }
}

impl std::fmt::Display for Board {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        for y in 0..BOARD_SIZE {
            let mut line:[u8;BOARD_SIZE] = [0u8;BOARD_SIZE];
            for x in 0..BOARD_SIZE {
                let loc = (y * BOARD_SIZE) + x;
                line[x] = self.get(loc as u32) as u8;
            }
            writeln!(f, "[{}] {:?}", y, line).unwrap();
        }
        Ok(())
    }
}


pub fn make_line_bits(path:&str) -> Result<()> {
    let mut bits:[u128;54] = [0u128;54];
    for i in 0..bits.len() {
        let locs = &LINE_LOCS[i];
        let mut bit = BOARD_HEAD_BIT >> locs[0];
        for loc_i in 1..BOARD_SIZE {
            let loc = locs[loc_i];
            if loc == locs[0] {break}
            bit |= BOARD_HEAD_BIT >> loc;
        }
        bits[i] = bit;
    }
    let mut lines:Vec<String> = vec!["[".into()];
    for i in 0..bits.len() {
        lines.push(format!("    {:#034X},   // {:?}", bits[i], LINE_LOCS[i]));
    }
    lines.push("];".into());
    std::fs::write(path, lines.join("\n")).unwrap();
    Ok(())
}

pub fn make_states(path:&str) -> Result<()> {
    fn decode(state:u32) -> [u32;BOARD_SIZE] {
        let mut ps = [0u32; BOARD_SIZE];
        let mut a = state;
        for i in 0..BOARD_SIZE {
            ps[i] = a % 3;
            a /= 3;
        }
        ps
    }
    fn make_board(ps:&[u32;BOARD_SIZE]) -> Board {
        let mut board = Board::default();
        for i in 0..BOARD_SIZE {
            board.set(i as Loc, PIECE_ORDER_MAP[ps[i] as usize]);
        }
        board
    }
    let mut states:[u32;59049] = [0u32; 59049];
    let mut pss:Vec<[u32;BOARD_SIZE]> = Vec::new();
    for state in 0..states.len() {
        let ps = decode(state as u32);
        let board = make_board(&ps);
        let mut bits:u32 = 0;
        for i in 0..BOARD_SIZE {
            let loc = i as Loc;
            if board.can_move(loc, Piece::Black) {
                bits |= 1 << (10+i);
            }
            if board.can_move(loc, Piece::White) {
                bits |= 1 << i;
            }
        }
        pss.push(ps);
        states[state] = bits;
    }
    let mut lines:Vec<String> = vec!["[".into()];
    for i in 0..states.len() {
        lines.push(format!("    {:#07X},   // {:?}", states[i], pss[i]));
    }
    lines.push("];".into());
    std::fs::write(path, lines.join("\n")).unwrap();
    Ok(())
}

#[cfg(test)]
mod tests{
    use super::*;
    use std::time::Duration;
    use crate::{log, utils::pftest};

    #[test]
    fn base() -> Result<()> {
        log::_init_for_test()?;
        let check_base_moves = ||{
            let board = Board::default();
            for loc in 0..BOARD_LEN as Loc {
                assert_eq!(board.get(loc), Piece::Empty);
            }
            assert_eq!(board.n_black(), 0);
            assert_eq!(board.n_white(), 0);
            assert_eq!(board.moves(Piece::Black), []);
            assert_eq!(board.moves(Piece::White), []);
            let mut board = Board::make_init_board();
            assert_eq!(board.n_black(), 2);
            assert_eq!(board.n_white(), 2);
            let locs = board.moves(Piece::Black);
            for loc in locs {
                assert!(board.can_move(loc, Piece::Black));
            }
            let locs = board.moves(Piece::White);
            for loc in locs {
                assert!(board.can_move(loc, Piece::White));
            }
            board.down(35, Piece::Black);
            assert_eq!(board.moves(Piece::Black), [53, 63, 64]);
            assert_eq!(board.moves(Piece::White), [34, 36, 56]);
            assert_eq!(board.get(35), Piece::Black);
            assert_eq!(board.get(45), Piece::Black);
            assert_eq!(board.n_black(), 4);
            assert_eq!(board.n_white(), 1);
        };
        log::test!("test: base - base moves");
        check_base_moves();

        fn make_can_moves(pcur:Piece, board:&Board) -> Vec<Loc> {
            let mut locs = Vec::new();
            for loc in 0..BOARD_LEN as Loc {
                if board.can_move(loc, pcur) {locs.push(loc);}
            }
            locs
        }
        fn check_moves(pcur:Piece, locs:&Vec<Loc>, board:&Board) {
            let mut loc_bits:u128 = 0;
            for loc in locs {
                loc_bits.set_bit_le(*loc);
            }
            for loc in 0..BOARD_LEN as Loc {
                let loc_is_can_move = board.can_move(loc, pcur);
                if loc_bits.test_bit_le(loc) {
                    let loc_is_empty = board.get(loc) == Piece::Empty;
                    if ! loc_is_empty {
                        log::test!("board:\n{board}\npcur:{pcur} loc:{loc}\ncan-moves:{:?}\nmoves:{:?}\nloc can't be empty", make_can_moves(pcur, board), locs);
                    }
                    assert!(loc_is_empty);
                    if ! loc_is_can_move {
                        log::test!("board:\n{board}\npcur:{pcur} loc:{loc}\ncan-moves:{:?}\nmoves:{:?}\nloc can't move, but in moves", make_can_moves(pcur, board), locs);
                    }
                    assert!(loc_is_can_move);
                } else if loc_is_can_move{
                    log::test!("board:\n{board}\npcur:{pcur} loc:{loc}\ncan-moves:{:?}\nmoves:{:?}\nloc can move, but not in moves", make_can_moves(pcur, board), locs);
                    assert!(! loc_is_can_move)
                }
            }
        }
        log::test!("test: base - move generator");
        for _ in 0..16384 {
            let board = Board::make_random_board_by_init(4, BOARD_LEN);
            check_moves(Piece::Black, &board.moves(Piece::Black), &board);
            check_moves(Piece::White, &board.moves(Piece::White), &board);
        }
        Ok(())
    }

    #[test]
    fn line_bits() -> Result<()> {
        log::_init_for_test()?;
        log::test!("test: table - line bits");
        for i in 0..LINE_LOCS.len() {
            let bit_locs:Vec<u8> = loc_bits_to_vec(LINE_BITS[i]).iter().map(|a|{*a as u8}).collect();
            let order_locs = LINE_LOCS[i];
            log::test!("[{}] bit:{:?} orders:{:?}", i, bit_locs, order_locs);
            assert_eq!(bit_locs[0], order_locs[0]);
            for loc_i in 1..BOARD_SIZE {
                if order_locs[loc_i] == order_locs[0] {break}
                assert_eq!(bit_locs[loc_i], order_locs[loc_i]);
            }
        }
        Ok(())
    }

    #[test]
    fn pf_random_game() -> Result<()> {
        log::_init_for_test()?;
        let fn_run = move || {
            let mut board = Board::make_init_board();
            let mut n_pass = 0;
            let mut n_piece = board.n_black() + board.n_white();
            let mut side = if rand::random_bool(0.5) {Piece::Black} else {Piece::White};
            while (n_pass < 1) && (n_piece < BOARD_LEN) {
                let moves = board.moves(side);
                if moves.is_empty() {
                    n_pass += 1;
                } else {
                    let loc = moves[rand::random_range(0..moves.len())];
                    board.down(loc, side);
                    n_pass = 0;
                    n_piece += 1;
                }
                side = side.side();
            }
        };
        let max_time = Duration::from_secs(1);
        log::test!("pf-random-game per-{:?}:\n{:?}", max_time, pftest::per_time(fn_run, max_time));
        Ok(())
    }

    #[test]
    fn pf_random_game_cached() -> Result<()> {
        log::_init_for_test()?;
        let init_board = Board::make_random_board_by_init(10, 50);
        let fn_run = move || {
            let mut board = init_board.clone();
            let mut n_pass = 0;
            let mut n_piece = board.n_black() + board.n_white();
            let mut side = if rand::random_bool(0.5) {Piece::Black} else {Piece::White};
            while (n_pass < 1) && (n_piece < BOARD_LEN) {
                let moves = board.moves(side);
                if moves.is_empty() {
                    n_pass += 1;
                } else {
                    let loc = moves[rand::random_range(0..moves.len())];
                    board.down(loc, side);
                    n_pass = 0;
                    n_piece += 1;
                }
                side = side.side();
            }
        };
        let n_pre_build = 65536;
        log::test!("cache pre-build n:{}", n_pre_build);
        for _ in 0..n_pre_build {
            fn_run();
        }
        let max_time = Duration::from_secs(1);
        log::test!("pf-random-game-cached per-{:?}:\n{:?}", max_time, pftest::per_time(fn_run, max_time));
        Ok(())
    }


    #[test]
    fn pf_board_io() -> Result<()> {
        log::_init_for_test()?;
        let board = Board::make_random_board_by_init(10, 70);
        let black_locs = board.moves(Piece::Black);
        let white_locs = board.moves(Piece::White);
        if black_locs.is_empty() || white_locs.is_empty() {
            pf_board_io()
        } else {
            log::test!("pf-board-io init-board n-b:{} n-w:{}\n{board}\nmoves-b:{:?}\nmoves-w:{:?}", board.n_black(), board.n_white(), board.moves(Piece::Black), board.moves(Piece::White));
            let max_time = Duration::from_secs(1);
            {
                log::test!("pf-board-io per-1s n-piece:\n{:?}", pftest::per_time(move ||{
                    #[allow(unused)]
                    let n_black = board.n_black();
                    #[allow(unused)]
                    let n_white = board.n_white();
                }, max_time));
            }
            {
                let b = black_locs[0];
                let w = white_locs[0];
                log::test!("pf-board-io per-1s swap:\n{:?}", pftest::per_time(move ||{
                    let mut swap_board = board.clone();
                    swap_board.swap(b, Piece::Black);
                    swap_board.load(&board);
                    swap_board.swap(w, Piece::White);
                }, max_time));
            }
            {
                log::test!("pf-board-io per-1s can-moves:\n{:?}", pftest::per_time(move ||{
                    #[allow(unused)]
                    let mut blacks:[u8;BOARD_LEN] = [0u8;BOARD_LEN];
                    for loc in 0..BOARD_LEN as Loc {
                        blacks[loc as usize] = board.can_move(loc, Piece::Black) as u8;
                    }
                    #[allow(unused)]
                    let mut whites:[u8;BOARD_LEN] = [0u8;BOARD_LEN];
                    for loc in 0..BOARD_LEN as Loc {
                        whites[loc as usize] = board.can_move(loc, Piece::White) as u8;
                    }
                }, max_time));
            }
            {
                log::test!("pf-board-io per-1s moves:\n{:?}", pftest::per_time(move ||{
                    #[allow(unused)]
                    let blacks = board.moves(Piece::Black);
                    #[allow(unused)]
                    let whites = board.moves(Piece::White);
                }, max_time));
            }
            Ok(())
        }
    }
}
